/*******************************************************************************
 * Copyright 2011 Mile Orešković
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package com.mileoresko.hierarchism.testhsms.course;

import java.util.HashSet;
import java.util.Iterator;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.mileoresko.hierarchism.HSMBareState;
import com.mileoresko.hierarchism.HSMChoicePseudostateAdapter;
import com.mileoresko.hierarchism.HSMEvent;
import com.mileoresko.hierarchism.HSMState;
import com.mileoresko.hierarchism.HSMTerminalState;
import com.mileoresko.hierarchism.HierarchicalStateMachine;
import com.mileoresko.hierarchism.exceptions.HSMCommonTransitionDefinitionException;
import com.mileoresko.hierarchism.exceptions.HSMDefinitionException;
import com.mileoresko.hierarchism.exceptions.HSMDuplicateTransitionException;
import com.mileoresko.hierarchism.exceptions.HSMInternalTransitionDefinitionException;
import com.mileoresko.hierarchism.exceptions.HSMPseudoChoiceDefinitionException;

public class Enrollment extends HierarchicalStateMachine {

    private static final Logger logger = LoggerFactory.getLogger("Enrollment");

    public static class EnrollmentEvent extends HSMEvent {
        public EnrollmentEvent(final String label) {
            super(label);
        }
    }

    public static class EnrollmentEvents {
        public static final EnrollmentEvent CANCEL = new EnrollmentEvent("Cancel");
        public static final EnrollmentEvent CLOSE = new EnrollmentEvent("Close");
        public static final EnrollmentEvent DROP_STUDENT = new EnrollmentEvent("DropStudent");
        public static final EnrollmentEvent ENROLL_STUDENT = new EnrollmentEvent("EnrollStudent");
        public static final EnrollmentEvent OPEN = new EnrollmentEvent("Open");
        public static final EnrollmentEvent SCHEDULE = new EnrollmentEvent("Schedule");
    }

    public class EnrollmentState extends HSMState {

        protected EnrollmentState(final String label) {
            super(Enrollment.this, label);
        }

        protected final void defineNestedHsms() {
            // There will be no nested HSMs in any of these states
        }

        /**
         * A default empty entryAction implementation is provided for convenience.
         * Override where needed.
         */
        protected void entryAction(final Object arg) {

        }

        /**
         * A default empty exitAction implementation is provided for convenience.
         * Override where needed.
         */
        protected void exitAction(final Object arg) {

        }
    }

    public class EnrollmentStates {
        public final EnrollmentState PROPOSED = new EnrollmentState("Proposed");

        public final EnrollmentState SCHEDULED = new EnrollmentState("Scheduled") {
            protected void entryAction(final Object arg) {
                super.entryAction(arg);
                scheduleEnrollment((String) arg);
            }
        };

        public final EnrollmentState OPEN_FOR_ENROLLMENT = new EnrollmentState("OpenForEnrollment") {
            protected void entryAction(final Object arg) {
                super.entryAction(arg);
                logOpenEnrollment();
            }
        };

        public final EnrollmentState FULL_TO_CAPACITY = new EnrollmentState("FullToCapacity") {
            protected void entryAction(final Object arg) {
                super.entryAction(arg);
                logFullEnrollment();
            }
        };

        public final EnrollmentState CLOSED_FOR_ENROLLMENT = new EnrollmentState("ClosedForEnrollment") {
            protected void entryAction(final Object arg) {
                super.entryAction(arg);
                notifyInstructor();
            }
        };

        public final HSMTerminalState CANCELLED = new HSMTerminalState(Enrollment.this, "Cancelled");
    }

    private final HashSet studentsEnrolled;
    private final HashSet waitingList;

    private int groupsCount;

    private final String courseName;
    private String date;

    private final int maxWaitingListSize;
    private final int maxGroupSize;

    private final EnrollmentStates states;

    private final HSMChoicePseudostateAdapter onStudentDropOpen = new HSMChoicePseudostateAdapter() {
        public HSMBareState choicePseudostateTransition(final Object arg) {
            dropStudent((Student) arg);
            if (isExistingCourseRedundant()) {
                recreateWaitingList();
                groupsCount--;
                return states.FULL_TO_CAPACITY;
            } else {
                return states.OPEN_FOR_ENROLLMENT;
            }
        }
    };

    private final HSMChoicePseudostateAdapter onStudentDropFull = new HSMChoicePseudostateAdapter() {
        public HSMBareState choicePseudostateTransition(final Object arg) {
            dropStudent((Student) arg);
            if (isSeatAvailable()) {
                enrolFromWaitingList();
            }
            if (isSeatAvailable()) {
                return states.OPEN_FOR_ENROLLMENT;
            } else {
                return states.FULL_TO_CAPACITY;
            }
        }
    };

    private final HSMChoicePseudostateAdapter onStudentEnrollOpen = new HSMChoicePseudostateAdapter() {
        public HSMBareState choicePseudostateTransition(final Object arg) {
            enrollStudent((Student) arg);
            if (isSeatAvailable()) {
                return states.OPEN_FOR_ENROLLMENT;
            } else {
                return states.FULL_TO_CAPACITY;
            }
        }
    };

    private final HSMChoicePseudostateAdapter onStudentEnrollFull = new HSMChoicePseudostateAdapter() {
        public HSMBareState choicePseudostateTransition(final Object arg) {
            putStudentOnWaitingList((Student) arg);
            if (isNewCourseNeeded()) {
                createNewCourse();
                return states.OPEN_FOR_ENROLLMENT;
            } else {
                return states.FULL_TO_CAPACITY;
            }
        }
    };

    public Enrollment(final String label, final int maxGroupSize, final int maxWaitingListSize) {
        super(label);
        states = new EnrollmentStates();
        try {
            create();
        } catch (final HSMDefinitionException e) {
            e.printStackTrace();
            System.exit(-1);
        }
        groupsCount = 1;
        courseName = label;
        this.maxGroupSize = maxGroupSize;
        this.maxWaitingListSize = maxWaitingListSize;
        studentsEnrolled = new HashSet();
        waitingList = new HashSet(maxWaitingListSize + 1);
    }

    protected void defineTransitions() throws HSMDuplicateTransitionException,
            HSMInternalTransitionDefinitionException, HSMCommonTransitionDefinitionException,
            HSMPseudoChoiceDefinitionException {

        addCommonTransition(states.PROPOSED, EnrollmentEvents.SCHEDULE, states.SCHEDULED, null);
        addCommonTransition(states.PROPOSED, EnrollmentEvents.CANCEL, states.CANCELLED, null);

        addCommonTransition(states.SCHEDULED, EnrollmentEvents.OPEN, states.OPEN_FOR_ENROLLMENT, null);
        addCommonTransition(states.SCHEDULED, EnrollmentEvents.CANCEL, states.CANCELLED, null);

        addChoicePseudostate(states.OPEN_FOR_ENROLLMENT, EnrollmentEvents.ENROLL_STUDENT, onStudentEnrollOpen);
        addChoicePseudostate(states.OPEN_FOR_ENROLLMENT, EnrollmentEvents.DROP_STUDENT, onStudentDropOpen);
        addCommonTransition(states.OPEN_FOR_ENROLLMENT, EnrollmentEvents.CANCEL, states.CANCELLED, null);
        addCommonTransition(states.OPEN_FOR_ENROLLMENT, EnrollmentEvents.CLOSE, states.CLOSED_FOR_ENROLLMENT, null);

        addChoicePseudostate(states.FULL_TO_CAPACITY, EnrollmentEvents.ENROLL_STUDENT, onStudentEnrollFull);
        addChoicePseudostate(states.FULL_TO_CAPACITY, EnrollmentEvents.DROP_STUDENT, onStudentDropFull);
        addCommonTransition(states.FULL_TO_CAPACITY, EnrollmentEvents.CANCEL, states.CANCELLED, null);
        addCommonTransition(states.FULL_TO_CAPACITY, EnrollmentEvents.CLOSE, states.CLOSED_FOR_ENROLLMENT, null);

        addCommonTransition(states.CLOSED_FOR_ENROLLMENT, EnrollmentEvents.CANCEL, states.CANCELLED, null);

    }

    protected HSMState getInitialState() {
        return states.PROPOSED;
    }

    void createNewCourse() {
        final Iterator itr = waitingList.iterator();
        while (itr.hasNext()) {
            studentsEnrolled.add(itr.next());
        }
        waitingList.clear();
        groupsCount++;
    }

    void dropStudent(final Student student) {
        studentsEnrolled.remove(student);
        waitingList.remove(student);
        logStudentDropped(student);
    }

    boolean enrolFromWaitingList() {
        if (waitingList.isEmpty()) {
            return false;
        }
        final Iterator itr = waitingList.iterator();
        final Student luckyStudent = (Student) itr.next();
        studentsEnrolled.add(luckyStudent);
        waitingList.remove(luckyStudent);
        logStudentEnrolledFromWaitlist(luckyStudent);
        return true;
    }

    void enrollStudent(final Student student) {
        studentsEnrolled.add(student);
        logStudentEnrolled(student);
    }

    boolean isExistingCourseRedundant() {
        if (groupsCount > 1) {
            return studentsEnrolled.size() < (groupsCount - 1) * maxGroupSize + maxWaitingListSize;
        } else {
            return false;
        }
    }

    boolean isNewCourseNeeded() {
        return waitingList.size() >= maxWaitingListSize;
    }

    boolean isSeatAvailable() {
        return studentsEnrolled.size() < groupsCount * maxGroupSize;
    }

    void logFullEnrollment() {
        logger.debug("Currently " + groupsCount + " course(s) are full to capacity with a total of "
                + studentsEnrolled.size() + " enrolled students. Additional " + waitingList.size()
                + " students are on the waiting list.");
    }

    void logOpenEnrollment() {
        logger.debug("Currently " + studentsEnrolled.size() + " students are enrolled in " + groupsCount + " courses.");
    }

    void logStudentDropped(final Student student) {
        logger.debug("Dropped student: " + student.toString());
    }

    void logStudentEnrolled(final Student student) {
        logger.debug("Enrolled student: " + student.toString());
    }

    void logStudentEnrolledFromWaitlist(final Student student) {
        logger.debug("Enrolled student from waitlist: " + student.toString());
    }

    void logStudentPutOnWaitlist(final Student student) {
        logger.debug("Student put on waitlist: " + student.toString());
    }

    void notifyInstructor() {
        logger.debug("Sending e-mail to course instructor!");
        logger.debug("E-mail content:");
        logger.debug("    Dear Sir/Madam,");
        logger.debug("  ");
        logger.debug("  Your course '" + courseName + "', scheduled for '" + date + "' has been closed for enrollment.");
        logger.debug("  A total of " + getEnrolledCount() + " students have been enrolled for it, split into "
                + getCoursesCount() + " groups.");
        logger.debug("  " + getWaitingListCount() + " unlucky students remained stuck on the waiting list.");
        logger.debug("  ");
        logger.debug("  Yours truly,");
        logger.debug("    The Enrollment HSM");
    }

    void putStudentOnWaitingList(final Student student) {
        waitingList.add(student);
        logStudentPutOnWaitlist(student);
    }

    void recreateWaitingList() {
        final Iterator itr = studentsEnrolled.iterator();
        for (int i = 0; i < maxWaitingListSize - 1; i++) {
            final Student unluckyStudent = (Student) itr.next();
            waitingList.add(unluckyStudent);
            logStudentPutOnWaitlist(unluckyStudent);
        }
        final Iterator itr2 = waitingList.iterator();
        while (itr2.hasNext()) {
            studentsEnrolled.remove(itr2.next());
        }
    }

    void scheduleEnrollment(final String arg) {
        date = arg;
    }

    public int getCoursesCount() {
        return groupsCount;
    }

    public int getEnrolledCount() {
        return studentsEnrolled.size();
    }

    public int getMaxGroupSize() {
        return maxGroupSize;
    }

    public int getMaxWaitingListSize() {
        return maxWaitingListSize;
    }

    public int getWaitingListCount() {
        return waitingList.size();
    }

    public EnrollmentStates states() {
        return states;
    }
}
